use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{higher, is_wild};
use rustc_ast::{Attribute, LitKind};
use rustc_errors::Applicability;
use rustc_hir::{Arm, BorrowKind, Expr, ExprKind, Guard, Pat};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::source_map::Spanned;

use super::MATCH_LIKE_MATCHES_MACRO;

/// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!`
pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
    if let Some(higher::IfLet {
        let_pat,
        let_expr,
        if_then,
        if_else: Some(if_else),
    }) = higher::IfLet::hir(cx, expr)
    {
        find_matches_sugg(
            cx,
            let_expr,
            IntoIterator::into_iter([(&[][..], Some(let_pat), if_then, None), (&[][..], None, if_else, None)]),
            expr,
            true,
        );
    }
}

pub(super) fn check_match<'tcx>(
    cx: &LateContext<'tcx>,
    e: &'tcx Expr<'_>,
    scrutinee: &'tcx Expr<'_>,
    arms: &'tcx [Arm<'tcx>],
) -> bool {
    find_matches_sugg(
        cx,
        scrutinee,
        arms.iter().map(|arm| {
            (
                cx.tcx.hir().attrs(arm.hir_id),
                Some(arm.pat),
                arm.body,
                arm.guard.as_ref(),
            )
        }),
        e,
        false,
    )
}

/// Lint a `match` or `if let` for replacement by `matches!`
fn find_matches_sugg<'a, 'b, I>(
    cx: &LateContext<'_>,
    ex: &Expr<'_>,
    mut iter: I,
    expr: &Expr<'_>,
    is_if_let: bool,
) -> bool
where
    'b: 'a,
    I: Clone
        + DoubleEndedIterator
        + ExactSizeIterator
        + Iterator<
            Item = (
                &'a [Attribute],
                Option<&'a Pat<'b>>,
                &'a Expr<'b>,
                Option<&'a Guard<'b>>,
            ),
        >,
{
    if_chain! {
        if iter.len() >= 2;
        if cx.typeck_results().expr_ty(expr).is_bool();
        if let Some((_, last_pat_opt, last_expr, _)) = iter.next_back();
        let iter_without_last = iter.clone();
        if let Some((first_attrs, _, first_expr, first_guard)) = iter.next();
        if let Some(b0) = find_bool_lit(&first_expr.kind, is_if_let);
        if let Some(b1) = find_bool_lit(&last_expr.kind, is_if_let);
        if b0 != b1;
        if first_guard.is_none() || iter.len() == 0;
        if first_attrs.is_empty();
        if iter
            .all(|arm| {
                find_bool_lit(&arm.2.kind, is_if_let).map_or(false, |b| b == b0) && arm.3.is_none() && arm.0.is_empty()
            });
        then {
            if let Some(last_pat) = last_pat_opt {
                if !is_wild(last_pat) {
                    return false;
                }
            }

            // The suggestion may be incorrect, because some arms can have `cfg` attributes
            // evaluated into `false` and so such arms will be stripped before.
            let mut applicability = Applicability::MaybeIncorrect;
            let pat = {
                use itertools::Itertools as _;
                iter_without_last
                    .filter_map(|arm| {
                        let pat_span = arm.1?.span;
                        Some(snippet_with_applicability(cx, pat_span, "..", &mut applicability))
                    })
                    .join(" | ")
            };
            let pat_and_guard = if let Some(Guard::If(g)) = first_guard {
                format!("{} if {}", pat, snippet_with_applicability(cx, g.span, "..", &mut applicability))
            } else {
                pat
            };

            // strip potential borrows (#6503), but only if the type is a reference
            let mut ex_new = ex;
            if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = ex.kind {
                if let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind() {
                    ex_new = ex_inner;
                }
            };
            span_lint_and_sugg(
                cx,
                MATCH_LIKE_MATCHES_MACRO,
                expr.span,
                &format!("{} expression looks like `matches!` macro", if is_if_let { "if let .. else" } else { "match" }),
                "try this",
                format!(
                    "{}matches!({}, {})",
                    if b0 { "" } else { "!" },
                    snippet_with_applicability(cx, ex_new.span, "..", &mut applicability),
                    pat_and_guard,
                ),
                applicability,
            );
            true
        } else {
            false
        }
    }
}

/// Extract a `bool` or `{ bool }`
fn find_bool_lit(ex: &ExprKind<'_>, is_if_let: bool) -> Option<bool> {
    match ex {
        ExprKind::Lit(Spanned {
            node: LitKind::Bool(b), ..
        }) => Some(*b),
        ExprKind::Block(
            rustc_hir::Block {
                stmts: &[],
                expr: Some(exp),
                ..
            },
            _,
        ) if is_if_let => {
            if let ExprKind::Lit(Spanned {
                node: LitKind::Bool(b), ..
            }) = exp.kind
            {
                Some(b)
            } else {
                None
            }
        },
        _ => None,
    }
}
